// Copyright 2019 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package main

import (
	"encoding/json"
	"fmt"
	"html/template"
	"log"
	"os"
	"sort"
	"strings"
)

func main() {
	// the first argument here will be the name of the binary, so we ignore
	// argument 0 when looking for the filepath.
	if len(os.Args) != 2 {
		fmt.Fprintln(os.Stderr, "Expected one argument: filepath of json schema to read.")
		os.Exit(1)
	}

	data, err := os.ReadFile(os.Args[1])
	if err != nil {
		log.Fatal(err)
	}

	var input []JSONFacade
	if err := json.Unmarshal(data, &input); err != nil {
		log.Fatal(err)
	}

	sort.Slice(input, func(i, j int) bool {
		f1, f2 := input[i], input[j]
		if f1.Name != f2.Name {
			return f1.Name < f2.Name
		}
		return f1.Version > f2.Version
	})

	output := make([]TemplateFacade, len(input))
	for k, facade := range input {
		methods := make([]TemplateMethod, 0, len(facade.Schema.Properties))
		for k, prop := range facade.Schema.Properties {
			methods = append(methods, TemplateMethod{
				Name:        k,
				Description: prop.Description,
				Param:       prop.Properties.Params.Ref,
				Result:      prop.Properties.Result.Ref,
			})
		}
		sort.Slice(methods, func(i, j int) bool {
			return methods[i].Name < methods[j].Name
		})

		definitions := make([]TemplateDefinition, 0, len(facade.Schema.Definitions))
		for k, prop := range facade.Schema.Definitions {
			properties := make([]TemplateProperty, 0, len(prop.Properties))
			for k, v := range prop.Properties {
				ref := v.Ref.Ref
				if v.Type == "array" {
					if v.Items.Type != "" {
						ref = v.Items.Type
					} else {
						ref = v.Items.Ref
					}
				}
				properties = append(properties, TemplateProperty{
					Name:   k,
					Type:   v.Type,
					Format: v.Format,
					Ref:    ref,
				})
			}
			sort.Slice(properties, func(i, j int) bool {
				return properties[i].Name < properties[j].Name
			})

			definitions = append(definitions, TemplateDefinition{
				Name:       k,
				Type:       prop.Type,
				Properties: properties,
			})
		}
		sort.Slice(definitions, func(i, j int) bool {
			return definitions[i].Name < definitions[j].Name
		})

		output[k] = TemplateFacade{
			Name:        facade.Name,
			AvailableTo: facade.AvailableTo,
			Description: facade.Description,
			Version:     facade.Version,
			Methods:     methods,
			Definitions: definitions,
		}
	}

	t, err := template.New("").Funcs(tmplFuncs).Parse(htmlTmpl)
	if err != nil {
		log.Fatal(err)
	}
	if err := t.Execute(os.Stdout, output); err != nil {
		log.Fatal(err)
	}
}

// JSONFacade represents the facade in json schema form
type JSONFacade struct {
	Name        string
	Description string
	Version     int
	AvailableTo []string
	Schema      JSONFacadeSchema
}

// JSONFacadeSchema represents the facade schema object in json schema form
type JSONFacadeSchema struct {
	Type        string                      `json:"type"`
	Description string                      `json:"description"`
	Properties  map[string]PropertySchema   `json:"properties"`
	Definitions map[string]DefinitionSchema `json:"definitions"`
}

// PropertySchema represents the property schema object in json schema form
type PropertySchema struct {
	Description string   `json:"description"`
	Type        string   `json:"type"`
	Properties  Property `json:"properties"`
}

// Property represents the property object in json schema form
type Property struct {
	Description string
	Params      Ref
	Result      Ref
}

// Ref represents a reference to another object
type Ref struct {
	Ref         string `json:"$ref"`
	Type        string `json:"type"`
	Description string `json:"description"`
}

// DefinitionSchema represents a definition in json schema form
type DefinitionSchema struct {
	Ref
	Format               string                      `json:"format"`
	Properties           map[string]DefinitionSchema `json:"properties"`
	AdditionalProperties bool                        `json:"additionalProperties"`
	Required             []string                    `json:"required"`
	Items                Ref                         `json:"items"`
}

// TemplateFacade represents a facade for templating usage
type TemplateFacade struct {
	Name        string
	Description string
	Version     int
	AvailableTo []string
	Methods     []TemplateMethod
	Definitions []TemplateDefinition
}

// TemplateMethod represents a facade method for templating usage
type TemplateMethod struct {
	Name        string
	Description string
	Param       string
	Result      string
}

// TemplateDefinition represents a facade definition for templating usage
type TemplateDefinition struct {
	Name       string
	Type       string
	Properties []TemplateProperty
}

type TemplateProperty struct {
	Name   string
	Type   string
	Format string
	Ref    string
}

var htmlTmpl = `
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Juju API docs (autogenerated)</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<style>
nav, main, footer {
	padding-left: 350px;
}
.sidenav .user-view {
	height: 180px;
	width: 100%;
}
.sidenav img {
	margin: -20px 0 0 20px;
	height: 160px;
}
</style>
</head>
<body>
<header>
	<ul class="sidenav sidenav-fixed" style="transform: translateX(0px);">
		<li>
			<div class="user-view">
				<img class="center-align" src="" />
			</div>
		</li>
		<li><div class="divider"></div></li>
	{{range .}}
		<li><a href="#{{.Name}}">{{.Name}} v{{.Version}}</a></li>
	{{end}}
		<li><div class="divider"></div></li>
	</ul>
</header>
<nav>
	<div class="nav-wrapper">
		<a id="top" href="#top" class="brand-logo">Juju API facades</a>
	</div>
</nav>
<main>
	<blockquote>
	The following are API facades for the Juju API. Each facade is versioned to
	ensure that backwards and forwards compatibility is achievable when using
	various versions of CLI and controllers.
	</blockquote>
	<blockquote>
	Although each facade in the API is versioned, the parameters are not, so
	it's worth noting that definitions could potentially break if removal or
	modifying a definition occurs between versions of Facades.
	</blockquote>
	<hr />
	{{range .}}
	{{$facade_name:=.Name}}
		<h2 id="{{.Name}}"><a href="#{{.Name}}">{{.Name}}</a> v{{.Version}}</h2>
		<blockquote>{{.Description}}</blockquote>
		<h5>Facade Type</h5>
		<ol>
			{{range .AvailableTo}}
				<li>{{.}}</li>
			{{end}}
		</ol>
		<div style="float:right">
			<ul>
				<li>Jump to <a href="#{{.Name}}_definitions">Definitions</a></li>
				<li>Jump to <a href="#top">Top</a></li>
			</ul>
		</div>
		<div style="clear:both"></div>
		<table class="highlight">
			<tr>
				<th>Name</th>
				<th>Params</th>
				<th>Results</th>
				<th>Description</th>
			</tr>
			{{range .Methods}}
			<tr>
				<td>{{.Name}}</td>
				<td><a href="#{{$facade_name}}_{{.Param | defLink}}">{{.Param | defName}}</a></td>
				<td><a href="#{{$facade_name}}_{{.Result | defLink}}">{{.Result | defName}}</a></td>
				<td>{{.Description}}</td>
			</tr>
			{{end}}
		</table>
		<h3 id="{{.Name}}_definitions"><a href="#{{.Name}}_definitions">{{.Name}} Definitions</a></h3>
		<div style="float:right">
			<ul>
				<li>Jump to <a href="#{{.Name}}">Methods</a></li>
				<li>Jump to <a href="#top">Top</a></li>
			</ul>
		</div>
		<div style="clear:both"></div>
		{{range .Definitions}}
		<h4 id="{{$facade_name}}_definitions_{{.Name}}"><a href="#{{$facade_name}}_definitions_{{.Name}}">{{.Name}}</a></h4>
		<table class="highlight">
			<tr>
				<th>Name</th>
				<th>Type</th>
				<th>Format</th>
				<th>Reference</th>
			</tr>
			{{range .Properties}}
			<tr>
				<td>{{.Name}}</td>
				<td>{{.Type | typeRef .Ref}}</td>
				<td>{{.Format}}</td>
				<td><a href="#{{$facade_name}}_{{.Ref | defLink}}">{{.Ref | defName}}</a></td>
			</tr>
			{{end}}
		</table>
		{{end}}
		<hr />
	{{end}}
</main>
<footer class="page-footer docs-footer">
© Copyright © 2024 CC-BY-SA, Canonical Ltd
</footer>
</body>
</html>
`

var tmplFuncs = template.FuncMap{
	"join": func(sep string, ss []string) string {
		return strings.Join(ss, sep)
	},
	"defLink": func(name string) string {
		if strings.HasPrefix(name, "#/") {
			name = name[2:]
		}
		return strings.Replace(name, "/", "_", -1)
	},
	"defName": func(name string) string {
		return strings.TrimPrefix(name, "#/definitions/")
	},
	"typeRef": func(ref, typ string) string {
		if typ != "array" && strings.HasPrefix(ref, "#/") {
			return "Ref"
		}
		return typ
	},
}
